<template>
    <el-dialog v-model="properties.visible"  width="750" title="上传文件"
               :before-close="properties.handle.close"
               :close-on-click-modal="false"
               align-center draggable >
        <!--上传参数-->
        <el-form ref="paramsRef" :model="properties.data.params" v-show="false">
            <el-form-item label="上传来源" prop="from">
                <el-input v-model="properties.data.params.from"/>
            </el-form-item>
            <el-form-item label="上传者" prop="owner">
                <el-input v-model="properties.data.params.owner"/>
            </el-form-item>
            <el-form-item label="目标标识" prop="target">
                <el-input v-model="properties.data.params.target"/>
            </el-form-item>
            <el-form-item label="主机标识" prop="host">
                <el-input v-model="properties.data.params.host"/>
            </el-form-item>
            <el-form-item label="扩展参数1" prop="p1">
                <el-input v-model="properties.data.params.p1"/>
            </el-form-item>
            <el-form-item label="扩展参数2" prop="p2">
                <el-input v-model="properties.data.params.p2"/>
            </el-form-item>
            <el-form-item label="扩展参数3" prop="p3">
                <el-input v-model="properties.data.params.p3"/>
            </el-form-item>
            <el-form-item label="并发数" prop="limit">
                <el-input v-model="properties.data.params.limit"/>
            </el-form-item>
        </el-form>
        <el-row :gutter="20">
            <el-col :span="24">
                <el-table :data="properties.data.fileList" border height="60vh">
                    <el-table-column label="Delivery Info" style="background-color: transparent;height: 15vh">
                        <template #header>
                            <el-upload :class="`upload-dialog ${isView? 'is-disabled':''}`" ref="uploaderRef" v-model:file-list="properties.data.fileList"
                                       :auto-upload="false" :show-file-list="false"
                                       :on-change="properties.handle.change" :on-success="properties.handle.success"
                                       :on-progress="properties.handle.progress" :http-request="properties.handle.httpRequest"
                                       multiple drag :disabled="isView">
                                <el-icon class="el-icon--upload">
                                    <svg-icon name="el-icon-upload-filled"/>
                                </el-icon>
                                <div class="el-upload__text">拖拽文件到此处 或 <em>点击这里上传</em></div>
                            </el-upload>
                        </template>
                        <el-table-column>
                            <template #header>
                                <div style="height: 10px">
                                    <el-dropdown trigger="hover" v-if="!isView">
                                        <el-button icon="filter">选择</el-button>
                                        <template #dropdown>
                                            <el-dropdown-menu class="user-dropdown">
                                                <el-dropdown-item @click="properties.handle.selected('folder')"> 文件夹</el-dropdown-item>
                                                <el-dropdown-item @click="properties.handle.selected('file')"> 文件</el-dropdown-item>
                                            </el-dropdown-menu>
                                        </template>
                                    </el-dropdown>
                                    <el-button-group class="flies-btn-list">
                                        <el-button v-if="!isView" icon="upload-filled" plain @click="properties.handle.operation('upload')">上传</el-button>
                                        <el-button icon="delete-filled" plain @click="properties.handle.operation('clear')">清空</el-button>
                                    </el-button-group>
                                </div>
                            </template>
                            <el-table-column align="right" fixed="left" header-align="center" label="序 号" type="index" width="70">
                                <template #default="{ $index }">
                                    {{ $index + 1 }}
                                </template>
                            </el-table-column>
                            <el-table-column property="name" label="文件名" min-width="200" fixed="left" show-overflow-tooltip/>
                            <el-table-column property="size" label="大小" width="95">
                                <template #default="scope">
                                    {{ sizeFormat(scope.row.size) }}
                                </template>
                            </el-table-column>
                            <el-table-column property="status" label="进度" width="250">
                                <template #default="scope">
                                    <el-text type="info" v-if="scope.row.status === 'ready'">{{properties.handle.percentage(scope.row)}}</el-text>
                                    <el-text type="danger" v-else-if="scope.row.status === 'fail'">{{properties.handle.percentage(scope.row)}}</el-text>
                                    <el-text type="success" v-else-if="scope.row.status === 'success'">{{properties.handle.percentage(scope.row)}}</el-text>
                                    <el-text type="primary" v-else-if="scope.row.status === 'uploading'">{{properties.handle.percentage(scope.row)}}</el-text>
                                </template>
                            </el-table-column>
                            <el-table-column property="option" label="操作" width="90" fixed="right" header-align="center" align="center">
                                <template #default="scope">
                                    <el-button icon="el-icon-delete" text @click="properties.handle.operation('remove', scope.row)"/>
                                </template>
                            </el-table-column>
                        </el-table-column>
                    </el-table-column>
                </el-table>
            </el-col>
        </el-row>
    </el-dialog>
</template>

<script lang="ts">
import {mitt, env, sizeFormat} from '@/utils';
import axios from 'axios';
import SparkMD5 from 'spark-md5';
import {genFileId, UploadInstance} from 'element-plus';
import store from "@/store";
import {CustomUploadFile, CustomUploadProgressEvent, CustomUploadRawFile, CustomUploadRequestOptions} from "@/components/file-upload/interface";
import {defineComponent, onMounted, onUnmounted, reactive, ref} from "vue";

const {API_URL} = env;

export default defineComponent({
    props: {
        action: {
            type: String,
            default: `${API_URL}/file/upload`
        },
        size: {
            type: Number,
            default: 1 * 1024 * 1024
        },
    },
    setup(props) {
        const isView = ref<boolean>(false);
        const {user, token} = store.userStore;
        const uploaderRef = ref<UploadInstance>();
        const paramsRef = ref();
        const uploader = reactive({
            // 拆分文件，计算文件MD5值
            computed: (file: CustomUploadRawFile, progress: (o: any)=> void) => {
                return new Promise((resolve, reject) => {
                    let spark = new SparkMD5.ArrayBuffer();
                    let total = Math.ceil(file.size / props.size);
                    let blobSlice = File.prototype.slice;
                    let index = 0, start = 0, end = 0;

                    let fileReader = new FileReader();
                    fileReader.onload = (e) => {
                        if (['delete'].includes(file.status)) {
                            return reject(false);
                        }
                        spark.append(e.target?.result);
                        index ++;
                        progress({ status: 'reading', readed: index});
                        if (index < total) {
                            read();
                        } else {
                            file.data.id = spark.end();
                            file.data.index = index > 1? 0:-1;
                            return resolve(true);
                        }
                    }

                    const read = () => {
                        start = index * props.size;
                        end = start + props.size;
                        let blob= blobSlice.call(file, start, Math.min(end, file.size))
                        fileReader.readAsArrayBuffer(blob);
                        file.blobs.push(blob);
                    }

                    const init = () => {
                        file.blobs = [];
                        file.readed = 0;
                        file.loaded = 0;
                        file.total = total;
                    }

                    init(); // 初始化扩展信息
                    read(); // 文件拆分，并计算M5D值
                });
            },

            // 上传前校验（断点续传，秒传）
            verification: (file: CustomUploadRawFile): Promise<{[key:string]:any}> => {
                return new Promise((resolve) => {
                    axios({
                        url: props.action,
                        method: 'GET',
                        headers: {
                            authentication: token
                        },
                        params: file.data
                    }).then((resp) => {
                        if (resp.data.code === 200) {
                            resolve(resp.data)
                        } else {
                            resolve(resp.data)
                        }
                    }).catch((e)=>{
                        console.error('校验请求异常', e)
                        resolve({code: 500, message: '校验请求发送异常'})
                    });
                })
            },

            // 上传文件块
            uploading: (formData: FormData):Promise<{[key:string]:any}> => {
                return new Promise((resolve, reject) => {
                    axios({
                        url: props.action,
                        method: "POST",
                        headers: {
                            authentication: token,
                            'Content-Type': 'multipart/form-data'
                        },
                        data: formData
                    }).then((resp) => {
                        if (resp.data.code === 200) {
                            resolve(resp.data);
                        } else {
                            reject(resp.data);
                        }
                    }).catch(() => {
                        reject({code: 500, message: '上传请求发送异常'})
                    });
                })
            },

            // 合并文件块
            merge: (file: CustomUploadRawFile): Promise<{[key:string]:any}> => {
                file.status = 'merge';
                return new Promise((resolve) => {
                    axios({
                        url: props.action,
                        method: "PUT",
                        headers: {
                            authentication: token
                        },
                        params: file.data,
                    }).then(res => {
                        resolve(res.data)
                    }).catch(()=>{
                        resolve({code: 500, message: '合并请求发送异常'})
                    });
                })
            },
        })

        const properties = reactive({
            visible: false,
            data: {
                fileList: [],
                params: {
                    owner: '' as string,
                    from: null, // 来源于哪个模块上传
                    target: null, //目标标识
                    host: "",
                    p1: null,
                    p2: null,
                    p3: null,
                    limit: 3 // 并发上传上限
                },
            },
            handle: {
                change: (file: CustomUploadFile) => {
                    if (file.status === 'ready' && file.raw && !Object.prototype.hasOwnProperty.call(file.raw, 'data')) {
                        file.raw.data = {
                            id: file.uid,
                            name: file.name,
                            size: file.size,
                            index: 0,
                            ...properties.data.params,
                        }
                        if (file.webkitRelativePath) {
                            file.raw.data.relativePath = file.webkitRelativePath.substring(0, file.webkitRelativePath.lastIndexOf('/'));
                        }
                    }
                },
                progress: async (event: CustomUploadProgressEvent, file: CustomUploadFile) => {
                    // 时时更新文件状态以及文件处理进度
                    switch (event.status) {
                        case 'reading':
                            file['readed'] = event.readed;
                            break;
                        case 'uploading':
                            file['loaded'] = event.loaded
                            break;
                    }
                    if (file.raw && (file.raw.status = event.status) === 'merge') {
                        file.percentage = 100
                    }
                },
                httpRequest: async (content: CustomUploadRequestOptions) => {
                    let {file, onProgress} = content;
                    // 拆分文件，计算MD5值
                    if (await uploader.computed(file, onProgress)) {
                        // 校验文件信息(断点续传，秒传，文件过滤等)
                        let data = await uploader.verification(file as CustomUploadRawFile)
                        if (data.code !== 200)  {
                            return Promise.resolve(data);
                        } else {
                            let queue:Promise<{[key:string]:any}>[] = [], end = file.blobs.length - 1, result: any = {code: 200};
                            for (let i = 0; i <= end; i++) {
                                // 删除文件
                                if (['delete'].includes(file.status)) {
                                    return Promise.reject();
                                }
                                // 跳过已上传文件块
                                if ((data.exists || []).includes(i) || (data.exists || []).includes(String(i))) {
                                    onProgress({status: 'uploading', loaded: i + 1} as CustomUploadProgressEvent)
                                    continue;
                                }
                                // 每次3个请求并发上传文件块
                                let formData = new FormData();
                                let blob = file.blobs[i];
                                Object.entries(file.data).forEach(([k, v]) => {
                                    switch (k) {
                                        case 'size':
                                            formData.append(k, blob.size as any)
                                            break;
                                        case 'index':
                                            formData.append(k, (end === 0? v:i) as any)
                                            break;
                                        default:
                                            formData.append(k, v)
                                            break;
                                    }
                                })
                                formData.append('file', blob);

                                queue.push(uploader.uploading(formData));
                                if (queue.length === file.data.limit || i === end) {
                                    result = await Promise.all(queue).then((data) => {
                                        onProgress({status: 'uploading', loaded: i + 1} as CustomUploadProgressEvent)
                                        queue.splice(0)
                                        queue.length = 0
                                        return data[0];
                                    }).catch((data) => {
                                        return data;
                                    })
                                    // 一个文件块上传失败，整个文件直接退出上传
                                    if (result.code !== 200) break;
                                }
                            }

                            // 所有文件块上传完成之后，合并文件
                            if (result.code === 200 && end !== 0) {
                                onProgress({status: 'merge'} as CustomUploadProgressEvent)
                                result = await uploader.merge(file as CustomUploadRawFile);
                            }

                            // 不论上传成功与否，皆返回成功状态，在成功回调函数中详细处理
                            return Promise.resolve(result);
                        }
                    } else {
                        return Promise.reject();
                    }
                },
                // 文件上传成功/失败/删除正在上传的回调
                success: ({code, message}: {code: number, message: string}, file: CustomUploadFile) => {
                    if (code !== 200) {
                        file.status = 'fail';
                        file.message = message;
                    } else {
                        uploaderRef.value?.handleRemove(file);
                    }
                },
                close: () => {
                    paramsRef.value.resetFields();
                    properties.visible = false;
                },
                selected: (type: string) => {
                    let item = document.querySelectorAll('.upload-dialog input[name="file"]')[0] as HTMLElement;
                    type === 'file' ? item.removeAttribute('webkitdirectory') : item.setAttribute('webkitdirectory', '');
                    item.click();
                },
                operation: (type: string, file?: any) => {
                    switch (type) {
                        case "upload":
                            properties.data.fileList
                                .filter((item: CustomUploadFile) => item.status === 'fail')
                                .forEach((item: CustomUploadFile) => {
                                    console.warn(`错误文件${item.name}重新上传`)
                                    uploaderRef.value?.handleRemove(item);
                                    if (item.raw) {
                                        item.raw.uid = genFileId();
                                    }
                                    uploaderRef.value?.handleStart(item.raw as CustomUploadRawFile);
                                })
                            uploaderRef.value?.submit();
                            break;
                        case "clear":
                            properties.data.fileList.forEach((item: CustomUploadFile) => {
                                if (item.raw) {
                                    item.raw.status = 'delete';
                                }
                            })
                            uploaderRef.value?.clearFiles()
                            break;
                        case "remove":
                            if (['ready', 'fail', 'success'].includes(file.status)){
                                uploaderRef.value?.handleRemove(file);
                            } else {
                                file.raw.status = 'delete';
                            }
                            break
                    }
                },
                percentage: (file: any) => {
                    switch (file.status) {
                        case 'ready':
                            return '等待';
                        case 'success':
                            return '完成';
                        case 'fail':
                            return `失败(${file.message})`;
                        case 'uploading':
                            if (file.raw.status === 'uploading') {
                                return '正在上传(' + Math.round(file.loaded / file.raw.total * 100) + '%)'
                            } else if (file.raw.status === 'reading'){
                                return '正在加载(' + Math.round(file.readed / file.raw.total * 100) + '%)'
                            } else if (file.raw.status === 'merge') {
                                return '正在合并';
                            } else {
                                return '未知'
                            }
                    }
                }
            }
        });

        // 展示弹窗
        const show = (value: any) => {
            isView.value = value === 'header';
            if (typeof value === 'object'){
                Object.assign(properties.data.params, value);
                if (!properties.data.params.owner) {
                    properties.data.params.owner = String(user?.userId);
                }
            }
            properties.visible = true;
        };

        onMounted(() => {
            mitt.on('onGlobalFileUpload', (data) => show(data))
        })

        onUnmounted(() => {
            mitt.off('onGlobalFileUpload')
        })
        return {
            uploaderRef, paramsRef,
            properties, show, isView, sizeFormat
        };
    }
});
</script>

<style>

.upload-dialog .el-upload-dragger{
    padding: 20px  5px !important;
}

.upload-dialog.is-disabled .el-upload-dragger{
    cursor: no-drop;
}

.flies-btn-list {
    position: relative;
    float: right;
}

.el-upload-list__item .el-upload-list__item-info {
    margin-left: 0 !important;
    line-height: 25px;
}

.el-upload-list__item .el-progress__text {
    top: -23px !important;
    min-width: 0 !important;
}

.el-upload-list__item .el-progress {
    top: unset !important;
    bottom: 0;
}


</style>